import { RoomEnvironment, type TransformControls } from 'three/examples/jsm/Addons.js';
import {
  type Box3Helper,
  type BufferGeometry,
  type CameraHelper,
  Color,
  EquirectangularReflectionMapping,
  Fog,
  FogExp2,
  type GridHelper,
  type Material,
  MathUtils,
  type Mesh,
  MeshBasicMaterial,
  MeshNormalMaterial,
  OrthographicCamera,
  PerspectiveCamera,
  type SkeletonHelper,
  type Texture,
  type WebGPURenderer,
} from 'three/webgpu';

import type { Editor } from './Editor.ts';
import type { EditorControls } from './EditorControls.ts';

type DispatcherParams = {
  editor: Editor;
  renderFn: () => void;
  animateFn: () => void;
  controls: EditorControls;
  transformControls: TransformControls;
  selectionBox: Box3Helper;
  grid: GridHelper;
};

const initDispatchers = (dispatcherParams: DispatcherParams) => {
  const { editor, renderFn, animateFn, controls, transformControls, selectionBox, grid } = dispatcherParams;

  initEditorDispatcher(editor, renderFn, animateFn, controls, transformControls);
  initTransformControlsDispatcher(editor, transformControls, renderFn);
  initObjectDispatcher(editor, renderFn, controls, transformControls, selectionBox);
  initBgEnvDispatcher(editor, renderFn);
  initFogDispatcher(editor, renderFn);
  initViewportDispatcher(editor, renderFn, controls, grid);
};

const initEditorDispatcher = (
  editor: Editor,
  renderFn: () => void,
  animateFn: () => void,
  controls: EditorControls,
  transformControls: TransformControls,
) => {
  const { signals, scene, targetElement } = editor;

  signals.editorCleared.add(() => {
    controls.center.set(0, 0, 0);
    renderFn();
  });

  signals.rendererUpdated.add(() => {
    scene.traverse((child) => {
      if ((child as Mesh).material !== undefined) {
        (child as Mesh<BufferGeometry, Material>).material.needsUpdate = true;
      }
    });

    renderFn();
  });

  signals.rendererCreated.add(onRendererCreated);

  function onRendererCreated(newRenderer: WebGPURenderer) {
    controls.connect(newRenderer.domElement);
    transformControls.connect(newRenderer.domElement);

    newRenderer.setAnimationLoop(animateFn);
    newRenderer.setClearColor(0xaaaaaa);

    if (window.matchMedia) {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
      mediaQuery.addEventListener('change', (event) => {
        if (newRenderer === null) return;
        newRenderer.setClearColor(event.matches ? 0x333333 : 0xaaaaaa);

        renderFn();
      });

      newRenderer.setClearColor(mediaQuery.matches ? 0x333333 : 0xaaaaaa);
    }

    newRenderer.setPixelRatio(window.devicePixelRatio);
    newRenderer.setSize(targetElement.offsetWidth, targetElement.offsetHeight);
    updateAspectRatio(editor);
    renderFn();
  }

  signals.rendererDetectKTX2Support.add((ktx2Loader) => {
    ktx2Loader.detectSupport(editor.renderer);
  });

  signals.sceneGraphChanged.add(() => {
    renderFn();
  });

  signals.cameraChanged.add(() => {
    renderFn();
  });

  signals.windowResize.add(() => {
    if (!editor.renderer) return;
    updateAspectRatio(editor);
    editor.renderer.setSize(targetElement.offsetWidth, targetElement.offsetHeight);
    renderFn();
  });
};

const initTransformControlsDispatcher = (
  editor: Editor,
  transformControls: TransformControls,
  renderFn: () => void,
) => {
  const signals = editor.signals;

  // signals
  signals.transformModeChanged.add((mode) => {
    transformControls.setMode(mode);
    renderFn();
  });

  signals.snapChanged.add((dist) => {
    transformControls.setTranslationSnap(dist);
  });

  signals.spaceChanged.add((space) => {
    transformControls.setSpace(space);
    renderFn();
  });
};

const initBgEnvDispatcher = (editor: Editor, renderFn: () => void) => {
  // background

  const { signals, scene } = editor;

  signals.sceneBackgroundChanged.add(
    ({
      backgroundType,
      backgroundColor,
      backgroundTexture,
      backgroundEquirectangularTexture,
      backgroundColorSpace,
      backgroundBlurriness,
      backgroundIntensity,
      backgroundRotation,
    }) => {
      scene.background = null;

      switch (backgroundType) {
        case 'Color':
          scene.background = new Color(backgroundColor);

          break;

        case 'Texture':
          if (backgroundTexture) {
            backgroundTexture.colorSpace = backgroundColorSpace;
            backgroundTexture.needsUpdate = true;

            scene.background = backgroundTexture;
          }

          break;

        case 'Equirectangular':
          if (backgroundEquirectangularTexture) {
            backgroundEquirectangularTexture.mapping = EquirectangularReflectionMapping;
            backgroundEquirectangularTexture.colorSpace = backgroundColorSpace;
            backgroundEquirectangularTexture.needsUpdate = true;

            scene.background = backgroundEquirectangularTexture;
            scene.backgroundBlurriness = backgroundBlurriness;
            scene.backgroundIntensity = backgroundIntensity;
            scene.backgroundRotation.y = backgroundRotation * MathUtils.DEG2RAD;

            if (useBackgroundAsEnvironment) {
              scene.environment = scene.background as Texture;
              scene.environmentRotation.y = backgroundRotation * MathUtils.DEG2RAD;
            }
          }

          break;
      }

      renderFn();
    },
  );

  // environment

  let useBackgroundAsEnvironment = false;

  signals.sceneEnvironmentChanged.add(({ environmentType, environmentEquirectangularTexture }) => {
    scene.environment = null;

    useBackgroundAsEnvironment = false;

    switch (environmentType) {
      case 'Background':
        useBackgroundAsEnvironment = true;

        if (scene.background !== null && (scene.background as Texture).isTexture) {
          scene.environment = scene.background as Texture;
          scene.environment.mapping = EquirectangularReflectionMapping;
          scene.environmentRotation.y = scene.backgroundRotation.y;
        }

        break;

      case 'Equirectangular':
        if (environmentEquirectangularTexture) {
          scene.environment = environmentEquirectangularTexture as Texture;
          scene.environment.mapping = EquirectangularReflectionMapping;
        }

        break;

      case 'Room':
        scene.environment = editor.pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;

        break;
    }

    renderFn();
  });
};

const initFogDispatcher = (editor: Editor, renderFn: () => void) => {
  const { signals, scene } = editor;

  signals.sceneFogChanged.add(({ fogType, fogColor, fogNear, fogFar, fogDensity }) => {
    switch (fogType) {
      case 'None':
        scene.fog = null;
        break;
      case 'Fog':
        scene.fog = new Fog(fogColor, fogNear, fogFar);
        break;
      case 'FogExp2':
        scene.fog = new FogExp2(fogColor, fogDensity);
        break;
    }

    renderFn();
  });

  signals.sceneFogSettingsChanged.add(({ fogType, fogColor, fogNear, fogFar, fogDensity }) => {
    switch (fogType) {
      case 'Fog':
        (scene.fog as Fog).color.setHex(fogColor);
        (scene.fog as Fog).near = fogNear;
        (scene.fog as Fog).far = fogFar;
        break;
      case 'FogExp2':
        (scene.fog as FogExp2).color.setHex(fogColor);
        (scene.fog as FogExp2).density = fogDensity;
        break;
    }

    renderFn();
  });
};

const initViewportDispatcher = (editor: Editor, renderFn: () => void, controls: EditorControls, grid: GridHelper) => {
  const { signals, scene, sceneHelpers } = editor;
  signals.viewportCameraChanged.add(() => {
    const viewportCamera = editor.viewportCamera;

    updateAspectRatio(editor);

    // disable EditorControls when setting a user camera
    controls.enabled = viewportCamera === editor.camera;

    renderFn();
  });

  signals.viewportShadingChanged.add(() => {
    const viewportShading = editor.viewportShading;

    switch (viewportShading) {
      case 'solid':
        scene.overrideMaterial = null;
        break;

      case 'normals':
        scene.overrideMaterial = new MeshNormalMaterial();
        break;

      case 'wireframe':
        scene.overrideMaterial = new MeshBasicMaterial({ color: 0x000000, wireframe: true });
        break;
    }

    renderFn();
  });

  //
  signals.showHelpersChanged.add((appearanceStates) => {
    grid.visible = appearanceStates.gridHelper;

    sceneHelpers.traverse((object) => {
      switch (object.type) {
        case 'CameraHelper': {
          object.visible = appearanceStates.cameraHelpers;
          break;
        }

        case 'PointLightHelper':
        case 'DirectionalLightHelper':
        case 'SpotLightHelper':
        case 'HemisphereLightHelper': {
          object.visible = appearanceStates.lightHelpers;
          break;
        }

        case 'SkeletonHelper': {
          object.visible = appearanceStates.skeletonHelpers;
          break;
        }

        default: {
          // not a helper, skip.
        }
      }
    });

    renderFn();
  });

  signals.cameraResetted.add(updateAspectRatio);
};

const initObjectDispatcher = (
  editor: Editor,
  renderFn: () => void,
  controls: EditorControls,
  transformControls: TransformControls,
  selectionBox: Box3Helper,
) => {
  const { signals, scene, camera } = editor;

  signals.objectSelected.add((object) => {
    selectionBox.visible = false;
    transformControls.detach();

    if (object !== null && object !== scene && object !== camera) {
      selectionBox.box.setFromObject(object, true);

      if (selectionBox.box.isEmpty() === false) {
        selectionBox.visible = true;
      }

      transformControls.attach(object);
    }

    renderFn();
  });

  signals.objectFocused.add((object) => {
    controls.focus(object);
  });

  signals.geometryChanged.add((object) => {
    if (object !== undefined) {
      selectionBox.box.setFromObject(object, true);
    }
    renderFn();
  });

  signals.objectChanged.add((object) => {
    if (editor.selected === object) {
      selectionBox.box.setFromObject(object, true);
    }

    if (object instanceof PerspectiveCamera || object instanceof OrthographicCamera) {
      object.updateProjectionMatrix();
    }

    const helper = editor.helpers.get(object.id);

    if (helper !== undefined && (helper as SkeletonHelper).isSkeletonHelper !== true) {
      (helper as CameraHelper).update();
    }

    renderFn();
  });

  signals.objectRemoved.add(({ object }) => {
    controls.enabled = true; // see #14180
    if (object === transformControls.object) {
      transformControls.detach();
    }
  });

  signals.materialChanged.add(() => {
    renderFn();
  });
};

function updateAspectRatio(editor: Editor) {
  editor.cameras.forEach((camera) => {
    const aspect = editor.targetElement.clientWidth / editor.targetElement.clientHeight;

    if (camera instanceof PerspectiveCamera) {
      camera.aspect = aspect;
      camera.updateProjectionMatrix();
    } else if (camera instanceof OrthographicCamera) {
      camera.left = -aspect;
      camera.right = aspect;
      camera.updateProjectionMatrix();
    }

    const helper = editor.helpers.get(camera.id);
    if (helper) (helper as CameraHelper).update();
  });
}

export { initDispatchers };
